﻿using System;
using System.Threading;
using System.Diagnostics;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

namespace T3K.SketchEditor.AvrBuilder {
    public class Builder {

        Dictionary<string, string> VARS = new Dictionary<string, string>();

        //Defaults
        const string CFLAGS = @"-Wall -g2 -gstabs -Os -fpack-struct -fshort-enums -ffunction-sections -fdata-sections -ffreestanding -funsigned-char -funsigned-bitfields -mmcu=$(MMCU) -DF_CPU=$(F_CPU) $[INCLUDE_DIRS:%=-I$(SRC_ROOT)/%]";
        const string CXXFLAGS = @"-Wall -g2 -gstabs -Os -fpack-struct -fshort-enums -ffunction-sections -fdata-sections -ffreestanding -funsigned-char -funsigned-bitfields -fno-exceptions -mmcu=$(MMCU) -DF_CPU=$(F_CPU) $[INCLUDE_DIRS:%=-I$(SRC_ROOT)/%]";
        const string LDFLAGS = @"-Os -Wl,-gc-sections -mmcu=$(MMCU)"; //#-Wl,--relax

        public char ListSeparator = '|';
        public int Verbose = 0;

        public Builder() {
            this["TARGET"] = string.Empty;
        }

        private void WriteLine(string format, params object[] parameters) {

            string[] strParams;
            if (parameters != null)
                strParams = new string[parameters.Length];
            else
                strParams = new string[0];

            for (int i = 0; i < strParams.Length; i++)
                strParams[i] = Expand(parameters[i].ToString());

            format = Expand(format);

            Console.WriteLine(format, strParams);
        }

        private int Exec(string command, string workingDirectory, params string[] arguments) {

            command = Expand(command);
            workingDirectory = Expand(workingDirectory);

            if (arguments != null)
                for (int i = 0; i < arguments.Length; i++)
                    arguments[i] = Expand(arguments[i]);


            try {
                Process proc = new Process();
                proc.StartInfo.WorkingDirectory = workingDirectory;
                proc.StartInfo.FileName = command;

                if (arguments != null)
                    proc.StartInfo.Arguments = string.Join(" ", arguments);

                proc.StartInfo.CreateNoWindow = true;
                proc.StartInfo.RedirectStandardOutput = true;
                proc.StartInfo.RedirectStandardError = true;
                proc.StartInfo.UseShellExecute = false;
                proc.StartInfo.LoadUserProfile = true;
                proc.EnableRaisingEvents = true;
                proc.OutputDataReceived += new DataReceivedEventHandler(p_OutputDataReceived);
                proc.ErrorDataReceived += new DataReceivedEventHandler(p_OutputDataReceived);

                if (Verbose >= 3)
                    Console.WriteLine("{0} {1}", proc.StartInfo.FileName, proc.StartInfo.Arguments);

                proc.Start();
                proc.BeginErrorReadLine();
                proc.BeginOutputReadLine();
                proc.WaitForExit();
                int exitCode = proc.ExitCode;

                if (exitCode != 0)
                    throw new Exception(string.Format("Exit {0}", exitCode));

                return exitCode;

            } catch (Exception ex) {
                string error = string.Format(
                    "error running {0} {1} in {2} : {3}",
                    command, string.Join(" ", arguments), workingDirectory, ex.Message
                );
                throw new IOException(error, ex);
            }
        }

        void p_OutputDataReceived(object sender, DataReceivedEventArgs e) {
            if (!string.IsNullOrEmpty(e.Data))
                Console.WriteLine(e.Data);
        }

        void p_OutputErrorDataReceived(object sender, DataReceivedEventArgs e) {
            if (!string.IsNullOrEmpty(e.Data))
                Console.WriteLine(e.Data);
        }

        private static Regex reT = new Regex("\\$\\([^()]*(((?<Open>\\()[^()]*)+((?<Close-Open>\\))[^()]*)+)*(?(Open)(?!))\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        private static Regex reArgs = new Regex("\\$\\(([a-zA-Z0-9_]+)\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        private static Regex reListArgs = new Regex("\\$\\[([a-zA-Z0-9_]+):(.*)=(.*)\\]", RegexOptions.IgnoreCase | RegexOptions.Compiled );
        //private static Regex reListArgs = new Regex("\\${([a-zA-Z0-9_]+)\\s+[^}]}", RegexOptions.IgnoreCase | RegexOptions.Compiled);

        private string Expand(string def) {
            bool foundMatch = false;

            if (reArgs.IsMatch(def)) {
                def = reArgs.Replace(def, new MatchEvaluator(ReplaceArgs_A));
                foundMatch = true;
            }

            if (reListArgs.IsMatch(def)) {
                def = reListArgs.Replace(def, new MatchEvaluator(ReplaceArgs_B));
                foundMatch = true;
            }

            if (foundMatch)
                def = Expand(def);

            return def;
        }

        public string ReplaceArgs_A(Match match) {
            if (match.Groups.Count > 0) {
                string key = match.Groups[1].Value;
                return this[key];
            } else 
                return match.Value;
        }

        public string ReplaceArgs_B(Match match) {
            if (match.Groups.Count > 0) {
                string key = match.Groups[1].Value;
                string matchExp = match.Groups[2].Value;
                string expression = match.Groups[3].Value;
                string[] values = this[key].Split(ListSeparator);
                StringBuilder sbuilder = new StringBuilder();

                matchExp = string.Format("^{0}$", matchExp.Replace("%", "(.*)"));

                foreach (string v in values) {
                    string v2 = Regex.Replace(v, matchExp, expression.Replace("%", "$1"));
                    sbuilder.Append(v2);
                }

                return Expand(sbuilder.ToString());

            } else
                return match.Value;
        }

        public string this[string key] {
            get {
                if (VARS.ContainsKey(key))
                    return VARS[key];
                else
                    throw new ArgumentException(
                        string.Format("{0} is undefined", key)
                    );
            }
            set {
                if (!VARS.ContainsKey(key))
                    VARS.Add(key, value);
                else
                    VARS[key] = value;
            }
        }

        public bool Defined(string key) {
            return VARS.ContainsKey(key);
        }

        public void Make(string Task) {
            switch (Task.ToUpper()) {
                case "CLEAN":
                    Clean();
                    break;
                case "UPLOAD":
                    Upload();
                    break;
                case "BUILD":
                    Build();
                    PrintSize();
                    break;
                case "DUMPVARS":
                    DumpVars();
                    break;
                case "ALL":
                default:
                    Build();
                    PrintSize();
                    Upload();
                    break;
            }
        }

        public void Include(string filePath) {
            FileInfo fInfo = new FileInfo(filePath);
            int fileType = 0;
            if (fInfo.Exists) {
                WriteLine("including file '{0}'", fInfo.Name);
                StreamReader sr = new StreamReader(fInfo.OpenRead());
                string line = null;
                string lastLine = null;
                string var = null;
                while ((line = sr.ReadLine()) != null) {

                    line = line.Trim();

                    if (line.Length == 0 || line[0] == '#')
                        continue;

                    if (line.EndsWith("\\")) {
                        lastLine = line.Substring(0, line.Length - 1);
                        continue;
                    } else {
                        if (lastLine != null)
                            line = lastLine + line;

                        lastLine = null;
                    }

                    switch (fileType) {
                        case 1: 
                            this[var] += string.Format("{0}{1}", ((this[var].Trim() == string.Empty) ? string.Empty : "|"), line);
                            break;
                        case 2:
                            string[] tokens1 = line.Split(new char[] { '=' }, 2);
                            if (tokens1.Length > 1)
                                this[tokens1[0].Trim()] = tokens1[1].Trim();
                            break;
                        case 0:
                            if (line.EndsWith(":")) {
                                fileType = 1;
                                var = line.Substring(0, line.Length - 1).Trim();
                                this[var] = string.Empty;
                            } else if (line.IndexOf('=') > -1) {
                                fileType = 2;
                                string[] tokens2 = line.Split(new char[] { '=' }, 2);
                                if (tokens2.Length > 1)
                                    this[tokens2[0].Trim()] = tokens2[1].Trim();
                            }
                            break;
                    }

                }
            }
        }

        private void BuildDepsDir(DirectoryInfo dInfo) {
            if (dInfo.Exists) {

                DirectoryInfo buildDir = new DirectoryInfo(Path.Combine(this["BUILD_DIR"], dInfo.Name));
                if (!buildDir.Exists)
                    buildDir.Create();

                foreach (FileInfo fInfo in dInfo.GetFiles()) {
                    string source = string.Format("\"{0}/{1}\"", dInfo.FullName.Replace('\\', '/'), fInfo.Name);
                    string output = string.Format("\"{0}/{1}.d\"", buildDir.FullName.Replace('\\', '/'), fInfo.Name);

                    switch (fInfo.Extension.ToLower()) {
                        case ".c":
                            if (Verbose >= 1) WriteLine("$(CC): {0} -> {0}.d", fInfo.Name);
                            Exec("$(CC)", "$(WDIR)", string.Format("$(CFLAGS) -MM -MF {1} {0}", source, output)); //o <- C
                            break;
                        case ".cpp":
                            if (Verbose >= 1) WriteLine("$(CXX): {0} -> {0}.d", fInfo.Name);
                            Exec("$(CXX)", "$(WDIR)", string.Format("$(CXXFLAGS) -MM -MF {1} {0}", source, output)); //o <- CPP
                            break;
                    }
                }
            }
        }

        private void BuildDepsFile(FileInfo fInfo) {

            if (fInfo.Exists) {

                DirectoryInfo dInfo = fInfo.Directory;

                DirectoryInfo buildDir = new DirectoryInfo(Path.Combine(this["BUILD_DIR"], dInfo.Name));
                if (!buildDir.Exists)
                    buildDir.Create();

                string source = string.Format("\"{0}/{1}\"", dInfo.FullName.Replace('\\', '/'), fInfo.Name);
                string output = string.Format("\"{0}/{1}.d\"", buildDir.FullName.Replace('\\', '/'), fInfo.Name);

                switch (fInfo.Extension.ToLower()) {
                    case ".c":
                        if (Verbose >= 1) WriteLine("$(CC): {0} -> {0}.d", fInfo.Name);
                        Exec("$(CC)", "$(WDIR)", string.Format("$(CFLAGS) -MM -MF {1} {0}", source, output)); //o <- C
                        break;
                    case ".cpp":
                        if (Verbose >= 1) WriteLine("$(CXX): {0} -> {0}.d", fInfo.Name);
                        Exec("$(CXX)", "$(WDIR)", string.Format("$(CXXFLAGS) -MM -MF {1} {0}", source, output)); //o <- CPP
                        break;
                }
            }
        }

        private void BuildDir(DirectoryInfo dInfo) {
            if (dInfo.Exists) {

                DirectoryInfo buildDir = new DirectoryInfo(Path.Combine(this["BUILD_DIR"], dInfo.Name));
                if (!buildDir.Exists)
                    buildDir.Create();

                StringBuilder objFiles = new StringBuilder();
                List<string> lObjFiles = new List<string>();

                foreach (FileInfo fInfo in dInfo.GetFiles()) {

                    string source = string.Format("\"{0}/{1}\"", dInfo.FullName.Replace('\\', '/'), fInfo.Name);
                    string output = string.Format("\"{0}/{1}.o\"", buildDir.FullName.Replace('\\', '/'), fInfo.Name);

                    switch (fInfo.Extension.ToLower()) {
                        case ".c":
                            if (Verbose >= 2) WriteLine("$(CC): {0} -> {0}.o", fInfo.Name);
                            Exec("$(CC)", "$(WDIR)", string.Format("$(CFLAGS) -c {0} -o {1}", source, output)); //o <- C
                            objFiles.AppendFormat(" {0}", output);
                            lObjFiles.Add(output);
                            break;
                        case ".cpp":
                            if (Verbose >= 2) WriteLine("$(CXX): {0} -> {0}.o", fInfo.Name);
                            Exec("$(CXX)", "$(WDIR)", string.Format("$(CXXFLAGS) -c {0} -o {1}", source, output)); //o <- CPP
                            objFiles.AppendFormat(" {0}", output);
                            lObjFiles.Add(output);
                            break;
                    }
                }

                this["OBJ"] += objFiles.ToString();
            }
        }

        private void BuildFile(FileInfo fInfo) {

            if (fInfo.Exists) {

                StringBuilder objFiles = new StringBuilder();
                List<string> lObjFiles = new List<string>();
                DirectoryInfo dInfo = fInfo.Directory;

                DirectoryInfo buildDir = new DirectoryInfo(Path.Combine(this["BUILD_DIR"], dInfo.Name));
                if (!buildDir.Exists)
                    buildDir.Create();

                string source = string.Format("\"{0}/{1}\"", dInfo.FullName.Replace('\\', '/'), fInfo.Name);
                string output = string.Format("\"{0}/{1}.o\"", buildDir.FullName.Replace('\\', '/'), fInfo.Name);

                switch (fInfo.Extension.ToLower()) {
                    case ".c":
                        if (Verbose >= 2) WriteLine("$(CC): {0} -> {0}.o", fInfo.Name);
                        Exec("$(CC)", "$(WDIR)", string.Format("$(CFLAGS) -c {0} -o {1}", source, output)); //o <- C
                        objFiles.AppendFormat(" {0}", output);
                        lObjFiles.Add(output);
                        break;
                    case ".cpp":
                        if (Verbose >= 2) WriteLine("$(CXX): {0} -> {0}.o", fInfo.Name);
                        Exec("$(CXX)", "$(WDIR)", string.Format("$(CXXFLAGS) -c {0} -o {1}", source, output)); //o <- CPP
                        objFiles.AppendFormat(" {0}", output);
                        lObjFiles.Add(output);
                        break;
                }

                this["OBJ"] += objFiles.ToString();
            }
        }

        private void Build() {

            string[] sourceDirs = this["SOURCE_DIRS"].Split(ListSeparator);

            for (int i = 0; i < sourceDirs.Length; i++)
                sourceDirs[i] = Expand(sourceDirs[i]);

            foreach (string dir in sourceDirs) {
                DirectoryInfo dInfo = new DirectoryInfo(dir);
                BuildDepsDir(dInfo);
            }

            string[] projectSources = new string[0];

            if (Defined("PSRC"))
                projectSources = this["PSRC"].Split(ListSeparator);                                       

            foreach (string projectFile in projectSources) {
                FileInfo fInfo = new FileInfo(projectFile);
                BuildDepsFile(fInfo);
            }

            this["OBJ"] = string.Empty;

            foreach (string dir in sourceDirs) {
                DirectoryInfo dInfo = new DirectoryInfo(dir);
                BuildDir(dInfo);
            }

            foreach (string projectFile in projectSources) {
                FileInfo fInfo = new FileInfo(projectFile);
                BuildFile(fInfo);
            }

            if (Verbose >= 4) WriteLine("$[OBJ:%=%\\n]");

            Exec("$(AR)", "$(WDIR)", "rcs $(TARGET).a $(OBJ)"); //a
            Exec("$(CXX)", "$(WDIR)", "$(LDFLAGS) $(TARGET).a -o $(TARGET).elf "); //elf
            Exec("$(OBJCOPY)", "$(WDIR)", "-R .eeprom -O ihex $(TARGET).elf  $(TARGET).hex"); //hex;

        }

        private void Clean() {
            //Exec("$(RM)", "$(WDIR)", "$(RMFLAGS) $(DEPS) $(OBJ) $(TARGET).elf $(TARGET).hex $(TARGET).a");
            throw new NotImplementedException("CLEAN is not implemented");
        }

        private void PrintSize() {
            Exec("$(SIZE)", "$(WDIR)", "--format=avr --mcu=$(MMCU) $(TARGET).elf");
        }

        private void ResetBoard() {
            System.IO.Ports.SerialPort port = new System.IO.Ports.SerialPort(Expand("$(PORT)"));
            port.Open();
            port.DiscardInBuffer();
            port.DiscardOutBuffer();
            port.DtrEnable = false;
            System.Threading.Thread.Sleep(100);
            port.DtrEnable = true;
            port.Close();
            port.Dispose();
        }

        private void Upload() {
            ResetBoard();
            Exec("$(AVRDUDE)", "$(WDIR)", "$(AVRDUDE_FLAGS)");
        }

        private void DumpVars() {
            foreach (string key in VARS.Keys) {
                Console.WriteLine("{0}: {1}", key, VARS[key]);
            }
        }

    }
}
